In [1]:
#Download basic example context
import urllib.request
url = "https://raw.githubusercontent.com/sjdv1982/seamless/master/examples/basic.seamless"
urllib.request.urlretrieve(url, filename = "basic.seamless")
Out[1]:
In [2]:
import seamless
from seamless import cell, pythoncell, reactor, transformer
ctx = seamless.fromfile("basic.seamless")
await ctx.computation()
Out[2]:
In [3]:
ctx.tofile("basic-copy.seamless", backup=False)
In the basic example, the code for the fibonacci function is defined in-line within the transformer. For a larger project that uses fibonacci in multiple places, you should define it separately. The standard way is to put it in a module and import it:
In [4]:
fib_module = open("fib.py", "w")
fib_module.write("""
def fibonacci(n):
def fib(n):
if n <= 1:
return [1]
elif n == 2:
return [1, 1]
else:
fib0 = fib(n-1)
return fib0 + [ fib0[-1] + fib0[-2] ]
fib0 = fib(n)
return fib0[-1]
""")
fib_module.close()
ctx.formula.set("""
from fib import fibonacci # Bad!
return fibonacci(a) + fibonacci(b)
""")
Out[4]:
But if we do this, we immediately lose live feedback. There is no way that seamless can guess that a change in fib.py should trigger a re-execution of ctx.formula's transformer. Even if you manually force a re-execution, with ctx.formula.touch(), this will not change anything: the fib module has already been imported by Python. Python's import mechanism is rather hostile to live code changes, and it is difficult to reload any kind of module. While possible to force manually (e.g. using %autoreload), it does not always work. Anyway, all this manual forcing is against the spirit of seamless.
With seamless, only use import for external libraries. Avoid importing any project code.
Instead of Python imports, seamless has a different mechanism: registrars.
First, let's link fib.py to a cell:
In [5]:
from seamless.lib import link, edit
ctx.fib = pythoncell()
ctx.link_fib = link(ctx.fib, ".", "fib.py") #Loads the cell from the existing fib.py
ctx.ed_fib = edit(ctx.fib, "Fib module")
Then, we will register the fib cell with the Python registrar, and connect the fibonacci Python function object from the Python registrar to the transformer.
This will re-establish live feedback: whenever fib.py gets changed, the transformer will execute with the new code.
In [6]:
rpy = ctx.registrar.python
rpy.register(ctx.fib)
rpy.connect("fibonacci", ctx.transform)
ctx.formula.set("return fibonacci(a) + fibonacci(b)")
Out[6]:
For the next section, we will build a new context.
You can destroy a context cleanly with context.destroy()
(Just re-defining ctx should work also, but not inside the Jupyter Notebook)
In [7]:
ctx.destroy()
In [8]:
import seamless
from seamless import cell, reactor, transformer
ctx = seamless.context()
ctx.x = cell("array")
ctx.y = cell("array")
In [9]:
import numpy as np
arr = np.linspace(0, 100, 200)
ctx.x.set(arr)
ctx.x.value[:10]
Out[9]:
In [10]:
arr2 = -0.5 * arr**2 + 32 * arr - 12
ctx.y.set(arr2)
Out[10]:
In [39]:
import bqplot
from bqplot import pyplot as plt
fig = plt.figure()
plt.plot(ctx.x.value, ctx.y.value)
plt.show()
Warning While cell.value, inputpins and editpins return numpy arrays, seamless assumes that you don't modify them in-place
In [12]:
t = ctx.computation = transformer({
"amplitude": {"pin": "input", "dtype": "float"},
"frequency": {"pin": "input", "dtype": "float"},
"gravity": {"pin": "input", "dtype": "float"},
"temperature": {"pin": "input", "dtype": "float"},
"mutation_rate": {"pin": "input", "dtype": "float"},
"x": {"pin": "input", "dtype": "array"},
"y": {"pin": "output", "dtype": "array"},
})
In [13]:
ctx.amplitude = cell("float").set(4)
ctx.amplitude.connect(t.amplitude)
ctx.frequency = cell("float").set(21)
ctx.frequency.connect(t.frequency)
ctx.gravity = cell("float").set(9.8)
ctx.gravity.connect(t.gravity)
ctx.temperature = cell("float").set(298)
ctx.temperature.connect(t.temperature)
ctx.mutation_rate = cell("float").set(42)
ctx.mutation_rate.connect(t.mutation_rate)
ctx.x.connect(t.x)
t.y.connect(ctx.y)
In [14]:
ctx.computation.code.cell().set("""
import numpy as np
import time
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 20):
pos = int(n/20*len(y))
return_preliminary(y[:pos])
time.sleep(1)
return y
""")
Out[14]:
In [15]:
ctx.computation.code.cell().touch()
Run the cell above, then repeatedly run the cell below
In [40]:
v = len(ctx.y.value)
print(v)
plt.clear()
plt.plot(ctx.x.value[:v], ctx.y.value)
plt.xlim(0,100)
plt.show()
Now let's assume that in the example above, we forgot a parameter "radius". To implement it, we would have to re-declare the transformer with the extra input pin, re-declare the connections, and re-define the code cells. This is very annoying, and it is easy to make a mistake!
However, transformer and reactor are macros, which means that they accept cells as input. So we can declare the computation parameters as a cell, and when we want to modify them, we just modify the cell.
Below is a re-factor:
In [17]:
ctx.computation_params = cell("json").set({
"amplitude": {"pin": "input", "dtype": "float"},
"frequency": {"pin": "input", "dtype": "float"},
"gravity": {"pin": "input", "dtype": "float"},
"temperature": {"pin": "input", "dtype": "float"},
"mutation_rate": {"pin": "input", "dtype": "float"},
"x": {"pin": "input", "dtype": "array"},
"y": {"pin": "output", "dtype": "array"},
})
In [18]:
t = ctx.computation = transformer(ctx.computation_params)
and then the same as before...
In [19]:
ctx.amplitude = cell("float").set(4)
ctx.amplitude.connect(t.amplitude)
ctx.frequency = cell("float").set(21)
ctx.frequency.connect(t.frequency)
ctx.gravity = cell("float").set(9.8)
ctx.gravity.connect(t.gravity)
ctx.temperature = cell("float").set(298)
ctx.temperature.connect(t.temperature)
ctx.mutation_rate = cell("float").set(42)
ctx.mutation_rate.connect(t.mutation_rate)
ctx.x.connect(t.x)
t.y.connect(ctx.y)
In [20]:
ctx.computation.code.cell().set("""
import numpy as np
import time
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 20):
pos = int(n/20*len(y))
return_preliminary(y[:pos])
time.sleep(1)
return y
""")
Out[20]:
In [21]:
ctx.computation.code.cell().touch()
Again, to see the plot, run the cell above, then repeatedly run the cell below
In [41]:
v = len(ctx.y.value)
print(v)
plt.clear()
plt.plot(ctx.x.value[:v], ctx.y.value)
plt.xlim(0,100)
plt.show()
Now we can add a parameter, and seamless will re-connect everything.
In [23]:
d = ctx.computation_params.value
d["radius"] = {"pin": "input", "dtype": "float"}
ctx.computation_params.set(d)
Out[23]:
In [24]:
ctx.radius = cell("float").set(10)
ctx.radius.connect(ctx.computation.radius)
This will restart the computation. If you like, you can now modify the code of ctx.computation.code.cell() to take into account the value of radius.
ctx.computation_params is a JSON cell. It can be linked to the hard disk and then edited to the hard disk like any other cell:
In [25]:
from seamless.lib import link
ctx.link1 = link(ctx.computation_params, ".", "computation_params.json")
Now, whenever you modify "computation_params.json", the transformer macro will be re-executed.
However, JSON is very unforgiving when it comes to commas and braces. Therefore, it is recommended that you declare ctx.computation_params as cell("cson") instead. In seamless, JSON and CSON have a special relationship: you can provide a CSON cell whenever a JSON cell is expected, and seamless will make the conversion implicitly.
Seamless macros can be declared with the @macro decorator. The following macro does the same as above:
In [26]:
from seamless import macro
@macro("json")
def create_computation(ctx, params):
from seamless import transformer, cell, pythoncell
ctx.computation = transformer(params)
ctx.computation_code = pythoncell().set("""
import numpy as np
import time
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 20):
pos = int(n/20*len(y))
return_preliminary(y[:pos])
time.sleep(1)
return y
""")
ctx.computation_code.connect(ctx.computation.code)
ctx.export(ctx.computation) #creates a pin on ctx for every unconnected pin on ctx.computation
ctx.computation = create_computation(ctx.computation_params)
Let's add a little convenience function to reconnect the computation pins:
In [27]:
def connect_computation(t):
ctx.amplitude = cell("float").set(4)
ctx.amplitude.connect(t.amplitude)
ctx.frequency = cell("float").set(21)
ctx.frequency.connect(t.frequency)
ctx.gravity = cell("float").set(9.8)
ctx.gravity.connect(t.gravity)
ctx.temperature = cell("float").set(298)
ctx.temperature.connect(t.temperature)
ctx.mutation_rate = cell("float").set(42)
ctx.mutation_rate.connect(t.mutation_rate)
ctx.radius = cell("float").set(10)
ctx.radius.connect(ctx.computation.radius)
ctx.x.connect(t.x)
t.y.connect(ctx.y)
connect_computation(ctx.computation)
... and plot the results
In [42]:
v = len(ctx.y.value)
print(v)
plt.clear()
plt.plot(ctx.x.value[:v], ctx.y.value)
plt.xlim(0,100)
plt.show()
The source code of the macro is added to the context, and it will be saved when the context is saved. Whenever ctx.computation_params changes, it will be re-executed.
In the next version of seamless, you will be able to edit the macro source code inside a cell. But for now, we have to just re-define it.
Let's assume that our scientific computation consists of two parts: a slow computation that depends only on amplitude and frequency, and a fast analysis of the result that depends on everything else. Using a macro, we can split the computation, and optionally omit the analysis.
In [29]:
@macro({"params": "json", "run_analysis": "bool"})
def create_computation(ctx, params, run_analysis):
from seamless import transformer, cell, pythoncell
from seamless.core.worker import ExportedOutputPin
# Slow computation
params_computation = {
"amplitude": {"pin": "input", "dtype": "float"},
"frequency": {"pin": "input", "dtype": "float"},
"x": {"pin": "input", "dtype": "array"},
"y": {"pin": "output", "dtype": "array"},
}
ctx.computation = transformer(params_computation)
ctx.computation_code = pythoncell().set("""
import numpy as np
import time
print("start slow computation")
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 5):
pos = int(n/5*len(y))
return_preliminary(y[:pos])
time.sleep(1)
return y
""")
ctx.computation_code.connect(ctx.computation.code)
ctx.computation_result = cell("array")
ctx.computation.y.connect(ctx.computation_result)
# Fast analysis
params2 = params.copy()
for k in params_computation:
if k not in ("x", "y"):
params2.pop(k, None)
ctx.analysis = transformer(params2)
ctx.analysis_code = pythoncell().set("print('start analysis'); return x")
ctx.analysis_code.connect(ctx.analysis.code)
# Final result
ctx.result = cell("array")
if run_analysis:
ctx.computation_result.connect(ctx.analysis.x)
ctx.analysis.y.connect(ctx.result)
else:
ctx.computation_result.connect(ctx.result)
ctx.y = ExportedOutputPin(ctx.result)
ctx.export(ctx.computation, skipped=["y"])
ctx.export(ctx.analysis, skipped=["x","y"])
ctx.run_analysis = cell("bool").set(True)
ctx.computation = create_computation(
params=ctx.computation_params,
run_analysis=ctx.run_analysis
)
connect_computation(ctx.computation)
As you see, the slow computation starts immediately. Every second, for five seconds, the computation returns the results so far. The results are forwarded to the analysis (which, in this dummy example, does nothing).
Now, if we change the radius parameter (or gravity, or temperature, or mutation_rate), the analysis will be re-executed, but not the slow computation
In [30]:
ctx.radius.set(2)
Out[30]:
On the other hand, changing amplitude or frequency re-launches the entire computation
In [31]:
ctx.amplitude.set(21)
Out[31]:
We can toggle run_analysis on and off, and the macro will re-build the computation context
In [32]:
ctx.run_analysis.set(False)
Out[32]:
In [33]:
ctx.run_analysis.set(True)
Out[33]:
Unfortunately, the re-building of the computation context also re-launches the slow computation.
However, seamless has (experimental!) caching for macros, which does not re-execute transformers whose inputs have not changed. It can be enabled with the with_caching parameter
In [34]:
@macro({"params": "json", "run_analysis": "bool"}, with_caching = True)
def create_computation(ctx, params, run_analysis):
# For the rest of the cell, as before ....
# ...
# ...
from seamless import transformer, cell, pythoncell
from seamless.core.worker import ExportedOutputPin
# Slow computation
params_computation = {
"amplitude": {"pin": "input", "dtype": "float"},
"frequency": {"pin": "input", "dtype": "float"},
"x": {"pin": "input", "dtype": "array"},
"y": {"pin": "output", "dtype": "array"},
}
ctx.computation = transformer(params_computation)
ctx.computation_code = pythoncell().set("""
import numpy as np
import time
print("start slow computation")
y = np.sin(x/100 * frequency) * amplitude
for n in range(1, 5):
pos = int(n/5*len(y))
return_preliminary(y[:pos])
time.sleep(1)
return y
""")
ctx.computation_code.connect(ctx.computation.code)
ctx.computation_result = cell("array")
ctx.computation.y.connect(ctx.computation_result)
# Fast analysis
params2 = params.copy()
for k in params_computation:
if k not in ("x", "y"):
params2.pop(k, None)
ctx.analysis = transformer(params2)
ctx.analysis_code = pythoncell().set("print('start analysis'); return x")
ctx.analysis_code.connect(ctx.analysis.code)
# Final result
ctx.result = cell("array")
if run_analysis:
ctx.computation_result.connect(ctx.analysis.x)
ctx.analysis.y.connect(ctx.result)
else:
ctx.computation_result.connect(ctx.result)
ctx.y = ExportedOutputPin(ctx.result)
ctx.export(ctx.computation, skipped=["y"])
ctx.export(ctx.analysis, skipped=["x","y"])
ctx.run_analysis = cell("bool").set(True)
ctx.computation = create_computation(
params=ctx.computation_params,
run_analysis=ctx.run_analysis
)
connect_computation(ctx.computation)
await ctx.computation()
Out[34]:
Now, when we toggle run_analysis, it will no longer re-run the computation
In [35]:
ctx.run_analysis.set(False)
await ctx.computation()
Out[35]:
In [36]:
ctx.run_analysis.set(True)
await ctx.computation()
Out[36]:
Jupyter has its own widget library for IPython kernels, called ipywidgets. In turn, several other visualization libraries, e.g. bqplot, are built upon ipywidgets.
ipywidgets uses traitlets to perform data synchronization. Below is a code snippet that uses seamless.observer and traitlets.observe to link a seamless cell to a traitlet (it will be included in the next seamless release).
In [37]:
import traitlets
from collections import namedtuple
import traceback
def traitlink(c, t):
assert isinstance(c, seamless.core.Cell)
assert isinstance(t, tuple) and len(t) == 2
assert isinstance(t[0], traitlets.HasTraits)
assert t[0].has_trait(t[1])
handler = lambda d: c.set(d["new"])
value = c.value
if value is not None:
setattr(t[0], t[1], value)
else:
c.set(getattr(t[0], t[1]))
def set_traitlet(value):
try:
setattr(t[0], t[1], value)
except:
traceback.print_exc()
t[0].observe(handler, names=[t[1]])
obs = seamless.observer(c, set_traitlet )
result = namedtuple('Traitlink', ["unobserve"])
def unobserve():
nonlocal obs
t[0].unobserve(handler)
del obs
result.unobserve = unobserve
return result
With this, we can create a nice little interactive dashboard for our scientific protocol:
In [38]:
# Clean up any old traitlinks, created by repeated execution of this cell
try:
for t in traitlinks:
t.unobserve()
except NameError:
pass
from IPython.display import display
from ipywidgets import Checkbox, FloatSlider
w_amp = FloatSlider(description = "Amplitude")
w_freq = FloatSlider(description = "Frequency")
w_ana = Checkbox(description="Run analysis")
traitlinks = [] # You need to hang on to the object returned by traitlink
traitlinks.append( traitlink(ctx.amplitude, (w_amp, "value")) )
traitlinks.append( traitlink(ctx.frequency, (w_freq, "value")) )
traitlinks.append( traitlink(ctx.run_analysis, (w_ana, "value")) )
import bqplot
from bqplot import pyplot as plt
fig = plt.figure()
plt.plot(np.zeros(1), np.zeros(1))
plt.xlim(0,100)
plt.ylim(-100,100)
traitlinks.append( traitlink(ctx.x, (fig.marks[0], "x")) )
traitlinks.append( traitlink(ctx.y, (fig.marks[0], "y")) )
display(w_amp)
display(w_freq)
display(w_ana)
display(fig)
ctx.run_analysis.set(False)
await ctx.computation()
Out[38]:
In [ ]: